package nl.unimaas.games.tron.player;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;

import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;

import org.w3c.dom.NamedNodeMap;

import nl.unimaas.games.tron.engine.Board;
import nl.unimaas.games.tron.engine.Evaluator.SpaceEvaluator;
import nl.unimaas.games.tron.player.mcts.CutOffStrategy;
import nl.unimaas.games.tron.player.mcts.FinalNodeSelectionStrategy;
import nl.unimaas.games.tron.player.mcts.LGRNGramPlayout;
import nl.unimaas.games.tron.player.mcts.LGRPlayout;
import nl.unimaas.games.tron.player.mcts.NGramPlayout;
import nl.unimaas.games.tron.player.mcts.Node;
import nl.unimaas.games.tron.player.mcts.SelectionStrategy;
import nl.unimaas.games.tron.util.GifSequenceWriter;

/**
 * 
 * @author Cliff Laschet
 */
@SuppressWarnings("serial")
public class LGRPBMCTSPlayer extends Player {

    protected boolean DEBUG = false;
    private final static boolean GFX_DEBUG = false;
    private final static double SIM_WIN = 1, SIM_DRAW = 0, SIM_LOSS = -1;
    private long MAX_TURN_TIME = 1000;
    private int turn = 0;
    private ArrayList<BufferedImage> debugImgs;
    protected Node root;
    protected Board rootBoard, currentBoard;
    protected double[][] winsBoard;
    protected double[][] visitsBoard;
    protected boolean applySpaceEstimation;
    protected boolean applyExpansionCutOff = false;
    protected boolean applySimulationCutOff = false;
    private boolean endgame = false;
    protected boolean syncMoves = false;
    protected boolean playoutSyncMoves = true;
    protected boolean LGRForgetting = true;
    protected boolean nGramSyncMoves = true;
    protected double alphaRandom = 0.8;
    protected LGRPlayout playout1 = new LGRPlayout(13, this.playoutSyncMoves, this.LGRForgetting);
    protected NGramPlayout playout2 = new NGramPlayout(13, this.alphaRandom, this.playoutSyncMoves);
    protected SelectionStrategy select = SelectionStrategy.RANDOM;//new SelectionStrategy.LGRRandomSelection(this.playout.getLGRTables(), syncMoves);
    protected FinalNodeSelectionStrategy finalMove = FinalNodeSelectionStrategy.SECURE_CHILD;
    protected CutOffStrategy cutOff = CutOffStrategy.SPACE;
    protected SpaceEvaluator spaceEval = new SpaceEvaluator();
    protected int reqSimulations = 60000; //Simulations performed before MCTS stops simulating.

    public LGRPBMCTSPlayer(String name, int num, Color c) {
        super(name, num, c);
    }

    @Override
    protected int computeMove(Board b, long endTime) {
        turn++;
        applySpaceEstimation = (applyExpansionCutOff || applySimulationCutOff);
        try {
            long start = System.currentTimeMillis();
            if (DEBUG) {
                System.out.println();
            }
            if (root == null) {//first move of the game 
                if (GFX_DEBUG) {
                    debugImgs = new ArrayList<BufferedImage>();
                }
                root = createNode(1 - getNumber(), null, Board.MOVE_NONE, b);
                endgame = false;
                onComputeFirstMove(root);
            }
            rootBoard = b;

            if (endTime == 0) {
                endTime = System.currentTimeMillis() + MAX_TURN_TIME;
            }

            int move = MCTS(root, endTime);
            //After each MCTS, reset the ngram. Board state has changed.
            this.playout1.resetLGRTables();
            this.playout2.resetNGram();
            
            if (DEBUG) {
                int pos = b.getPlayerPosition(getNumber());
                System.out.println("PERFORM MOVE " + Board.moveToString(move) + " (in " + (System.currentTimeMillis() - start) + " ms) FROM (" + Board.posToX(pos) + ", " + Board.posToY(pos) + ")");
                System.out.println("Time " + getNumber() + ": " + (System.currentTimeMillis() - start));
            }
            return move;
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("error in MCTSPlayer! " + e.getMessage());
            return Board.MOVE_NONE;
        }
    }

    @Override
    public synchronized void onPlayerMove(final Board b, int playerNr, int move) {
        if (playerNr == getNumber()) {
            return;
        }

        if (root.children.isEmpty()) {
            System.out.println();
            throw new IllegalStateException("root has no children");
        }
        for (int i = 0; i < root.children.size(); i++) {
            Node child = root.children.get(i);
            if (child.move == move) {
                child.parent.apply(rootBoard);
                //System.out.println("pred: " + child.prediction + " sdif: " + child.spaceDiff);
                child.parent = null;
                root = child;
                child.apply(rootBoard);
                if (!rootBoard.equals(b)) {
                    rootBoard.print();
                    b.print();
                    throw new IllegalStateException("boards do not match");
                }
                return;
            }
        }
        throw new IllegalStateException("cannot move to the correct state");
    }

    protected int MCTS(Node root, long endTime) {
        if (endTime - System.currentTimeMillis() < 100) {
            System.err.println("MCTS: I have less than 100 ms to compute a result!");
        }
        Node current;
        if (root.prediction != CutOffStrategy.UNDECIDED) {
            root.prediction = CutOffStrategy.UNDECIDED;
            root.expanded = false;
            endgame = true;
            System.out.println("endgame!!!");
        }

        currentBoard = rootBoard.deepClone(false);
        int its = 0, endIts = 0, cuts = 0, totalIts = 0;
        double r;

        //while (totalIts < this.reqSimulations) {
        while (System.currentTimeMillis() < endTime) {
            current = root;
            rootBoard.copyTo(currentBoard, false);

            /* Selection phase */
            while (!current.isLeaf() && current.expanded) {
                current = select(current);
            }

            /* Expansion phase */
            expand(current, currentBoard);

            if (current.prediction != CutOffStrategy.UNDECIDED) {
                cuts++;
            }

            r = 0;
            /* terminal state */
            if (current.children.isEmpty()) {
                int myNr = current.playerNr;
                int startingPlayer = myNr;
                int otherNr = 1 - myNr;
                if (myNr == getNumber()) {
                    startingPlayer = otherNr;
                }
                if (current.prediction == CutOffStrategy.UNDECIDED) {
                    //the other player has no more moves
                    int myPos = currentBoard.getPlayerPosition(current.playerNr);
                    int otherPos = currentBoard.getPlayerPosition(1 - current.playerNr);
                    if (myPos == otherPos) {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        r = SIM_DRAW; //crash -> draw
                    } else //check if we have any moves left
                    if (currentBoard.getValidMoveCount(Board.posToX(myPos), Board.posToY(myPos)) == 0) {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        r = SIM_DRAW; //both players are stuck -> draw
                    } else {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_WIN, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_WIN, syncMoves);
                        r = SIM_WIN; //we can still move -> win
                    }
                } else {
                    if (current.prediction == CutOffStrategy.CURRENT_PLAYER_WON) {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_WIN, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_WIN, syncMoves);
                        r = SIM_WIN;
                    } else if (current.prediction == CutOffStrategy.OTHER_PLAYER_WON) {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_LOSS, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_LOSS, syncMoves);
                        r = SIM_LOSS;
                    } else {
                        this.playout1.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        this.playout2.onFinishPlayout(startingPlayer, myNr, SIM_DRAW, syncMoves);
                        r = SIM_DRAW;
                    }
                }
                endIts++;
            } else {
                current = select(current);
                //simulation starts from one of our moves, player 2 moves first
                r = simulate(current, currentBoard);
                its++;
            }
            totalIts++;

            /* Backpropagation phase*/
            do {
                current.value += r;
                if (r == SIM_DRAW) {
                    current.draws++;
                } else if (r > SIM_DRAW) {
                    current.wins++;
                }

                current.visits++;
                current = current.parent;
                r *= -1;
            } while (current.parent != null);
            root.visits++;
        }

        //System.out.println("MCTS Simulations: " + its);
        if (DEBUG) {
            System.out.println("Simulations: " + its + "  Terminal node passes: " + endIts + " Total iterations: " + totalIts + " Cuts: " + cuts);
            System.out.println("Root visits: " + root.visits);
            System.out.println("Root children: ");
            root.printChildren();
        }
        if (GFX_DEBUG) {
            int w = rootBoard.getWidth(), h = rootBoard.getHeight();
            int scale = 32;
            winsBoard = new double[w][h];
            visitsBoard = new double[w][h];
            Board b = rootBoard.deepClone(false);
            createDebugBoard(root, b);

            //String file = "output" + File.separator + "mcts" + getNumber() + " (" + turn + ").gif";

            BufferedImage debugImg = new BufferedImage(w * 32, h * 32, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2 = debugImg.createGraphics();
            //get max
            double[][] debugBoard = visitsBoard;
            double max = Double.NEGATIVE_INFINITY;
            for (int x = 0; x < w; x++) {
                for (int y = 0; y < h; y++) {
                    if (debugBoard[x][y] > max) {
                        max = debugBoard[x][y];
                    }
                }
            }
            //max = 1;
            for (int x = 0; x < w; x++) {
                for (int y = 0; y < h; y++) {
                    Color color;
                    if (rootBoard.isWall(x, y)) {
                        color = Color.BLACK;
                    } else {
                        double factor = debugBoard[x][y] / max;
                        if (factor > 0) {
                            color = blend(Color.RED, Color.GREEN, factor);
                        } else {
                            color = Color.GRAY;
                        }
                    }
                    g2.setColor(color);
                    g2.fillRect(x * scale, y * scale, (int) scale, (int) scale);
                }
            }
            //Board.printBoardArray(winsBoard);
            g2.dispose();
            debugImg.flush();
            debugImgs.add(debugImg);

            /*
            try {
            File outputfile = new File(file);
            ImageIO.write(debugImg, "gif", outputfile);
            } catch (Exception e) {
            e.printStackTrace();
            }*/
        }

        /* Final node selection phase */
        root = finalMove.getNode(root);
        //System.out.println("UCT:");
        //root.parent.printChildren();
        if (root == null) {
            System.err.println("Got null from finalMove.getNode(root)!");
        } else if (root.children.isEmpty()) {
            System.err.println("New root has no children!");
        }
        this.root = root;
        onFinishComputation(root);
        //discard the upper part of the tree
        root.parent = null;
        if (DEBUG) {
            System.out.println("Best node: " + root);
        }
        return root.move;
    }

    private void createDebugBoard(Node n, Board b) {
        if (n.parent != null) {
            n.apply(b);
        }
        int mypos = b.getPlayerPosition(n.playerNr);
        double visits = visitsBoard[Board.posToX(mypos)][Board.posToY(mypos)];
        double wins = winsBoard[Board.posToX(mypos)][Board.posToY(mypos)];
        if (visits + n.visits > 0) {
            winsBoard[Board.posToX(mypos)][Board.posToY(mypos)] = (wins * visits + n.wins) / ((double) visits + n.visits);
        }
        visitsBoard[Board.posToX(mypos)][Board.posToY(mypos)] += n.visits;
        for (Node c : n.children) {
            createDebugBoard(c, b.deepClone(false));
        }
    }

    /** Selects a child node of the current node */
    protected Node select(Node node) {
        Node n = select.getNode(node);
        n.apply(currentBoard);
        return n;
    }

    protected void expand(Node node, Board board) {
        if (!node.expanded) {
            node.expanded = true;
            //the other player performs a move

            int myNr = 1 - node.playerNr; //player making the move
            //pre-create all nodes
            int myPos = board.getPlayerPosition(myNr);
            int myX = Board.posToX(myPos), myY = Board.posToY(myPos);

            ArrayList<Integer> moves = board.getValidMoves(myX, myY);
            if (!endgame && applyExpansionCutOff
                    && node.playerNr != getNumber()
                    && !moves.isEmpty()
                    && !node.isRoot()) {
                node.spaceDiff = spaceEval.compute(board, node.playerNr);
                node.connected = spaceEval.isConnected();
                node.prediction = cutOff.getEnding(node, board);
            }

            if (node.prediction == CutOffStrategy.UNDECIDED) {
                for (int m : moves) {
                    Node n = createNode(myNr, node, m, board);
                    node.children.add(n);
                }

                //since we moved first, the current player (2) might be able turn the game into a draw
                if (myNr != getNumber()) {
                    int otherNr = node.playerNr;
                    int otherPos = board.getPlayerPosition(otherNr);
                    int otherX = Board.posToX(otherPos), otherY = Board.posToY(otherPos);
                    if (Board.getManhattanDistance(myX, myY, otherX, otherY) == 1) {
                        int m = Board.getMoveFromPositions(myX, myY, otherX, otherY);
                        Node n = createNode(myNr, node, m, board);
                        n.expanded = true;
                        node.children.add(n);
                    }
                }
            }
        }
    }

    /** Simulates a game from the viewpoint of the player of the given node */
    protected double simulate(Node node, Board simBoard) {
        /*if (GFX_DEBUG) {
        int mypos = currentBoard.getPlayerPosition(node.playerNr);
        debugBoard[Board.posToX(mypos)][Board.posToY(mypos)]++;
        }*/
        int myMove, otherMove;

        int myNr = node.playerNr;
        int myPos = simBoard.getPlayerPosition(myNr);
        int myX = Board.posToX(myPos), myY = Board.posToY(myPos);
        //get position
        int otherNr = 1 - node.playerNr;
        int otherPos = simBoard.getPlayerPosition(otherNr);
        int otherX = Board.posToX(otherPos), otherY = Board.posToY(otherPos);
        
        //Set the history positions for ngrams
        /*int first = -1, second = -1;
        if(node.parent != null){
            first = node.parent.playerPosition;
        }
        second = node.playerPosition;
        this.playout.setPreviousMoves(first, second);*/

        int startingPlayer = myNr;

        if (myNr == getNumber()) {
            startingPlayer = otherNr;
            //player 2 still has to perform a move first
            otherMove = playout1.getMove(simBoard, otherX, otherY, otherNr);

            if (otherMove == Board.MOVE_NONE) {
                //check if he could've moved to our square, as that would make it a tie since it's his only option
                if (Board.getManhattanDistance(myX, myY, otherX, otherY) == 1) {
                    double reward = SIM_DRAW;
                    /*if (myNr != getNumber()) {
                        reward *= -1;
                    }*/
                    this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    return SIM_DRAW;
                } else {
                    double reward = SIM_WIN;
                    /*if (myNr != getNumber()) {
                        reward *= -1;
                    }*/
                    this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    return SIM_WIN; //he's stuck
                }
            }

            otherPos = Board.getPositionFromMove(otherX, otherY, otherMove);
            if (myPos == otherPos) {
                double reward = SIM_DRAW;
                /*if (myNr != getNumber()) {
                    reward *= -1;
                }*/
                this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                return SIM_DRAW; //tie
            }
            simBoard.performMove(otherX, otherY, otherMove, otherNr, false);
            otherX = Board.posToX(otherPos);
            otherY = Board.posToY(otherPos);
        }
        int it = 0;
        while (true) {
            myMove = playout1.getMove(simBoard, myX, myY, myNr);
            otherMove = playout1.getMove(simBoard, otherX, otherY, otherNr);

            if (myMove == Board.MOVE_NONE) {
                if (otherMove == Board.MOVE_NONE) {
                    double reward = SIM_DRAW;
                    /*if (myNr != getNumber()) {
                        reward *= -1;
                    }*/
                    this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    return SIM_DRAW; //both players did not move
                } else {
                    double reward = SIM_LOSS;
                    /*if (myNr != getNumber()) {
                        reward *= -1;
                    }*/
                    this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                    return SIM_LOSS; //I did not move
                }
            } else if (otherMove == Board.MOVE_NONE) {
                double reward = SIM_WIN;
                /*if (myNr != getNumber()) {
                    reward *= -1;
                }*/
                this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                return SIM_WIN; //other did not move
            }

            myPos = Board.getPositionFromMove(myX, myY, myMove);
            otherPos = Board.getPositionFromMove(otherX, otherY, otherMove);

            if (myPos == otherPos) {
                double reward = SIM_DRAW;
                /*if (myNr != getNumber()) {
                    reward *= -1;
                }*/
                this.playout1.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                this.playout2.onFinishPlayout(startingPlayer, myNr, reward, playoutSyncMoves);
                return SIM_DRAW;
            }

            //perform moves
            simBoard.performMove(myX, myY, myMove, myNr, true);
            simBoard.performMove(otherX, otherY, otherMove, otherNr, true);

            //update positions
            myX = Board.posToX(myPos);
            myY = Board.posToY(myPos);
            otherX = Board.posToX(otherPos);
            otherY = Board.posToY(otherPos);

            it++;
            if (applySimulationCutOff && it % 5 == 0) {
                double diff = spaceEval.compute(simBoard, myNr);
                if (diff != 0 && !Double.isNaN(diff)) {
                    return (diff > 0) ? SIM_WIN : SIM_LOSS;
                }
            }
        }
    }

    protected void onComputeFirstMove(Node theRoot) {
    }

    protected void onFinishComputation(Node newRoot) {
    }

    protected Node createNode(int player, Node parent, int move, Board b) {
        Node n;
        boolean max = (player == getNumber());
        if (parent == null) {
            n = new Node(player, max, b);
        } else {
            n = new Node(parent, player, move, max, b);
        }
        return n;
    }

    @Override
    public void reset() {
        if (GFX_DEBUG && debugImgs != null && !debugImgs.isEmpty()) {
            ImageOutputStream output;
            try {
                output = new FileImageOutputStream(new File("output" + File.separator + "mcts" + getNumber() + ".gif"));
                GifSequenceWriter writer = new GifSequenceWriter(output, debugImgs.get(0).getType(), 1000, true);
                for (int i = 0; i < debugImgs.size(); i++) {
                    BufferedImage nextImage = debugImgs.get(i);
                    writer.writeToSequence(nextImage);
                }
                writer.close();
                output.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            debugImgs = null;
        }

        turn = 0;
        root = null;
    }

    public static Player fromXml(org.w3c.dom.Node playerRoot, int num, Color color) {
        return fromXml(playerRoot, new LGRPBMCTSPlayer(LGRPBMCTSPlayer.class.getSimpleName(), num, color));
    }

    public static Player fromXml(org.w3c.dom.Node playerRoot, LGRPBMCTSPlayer player) {
        NamedNodeMap attrs = playerRoot.getAttributes();
        player.description = "";
        player.cutOff = null;
        player.applyExpansionCutOff = false;
        player.applySimulationCutOff = false;
        
        if (attrs.getNamedItem("playoutSyncMoves") != null) {
            try {
                player.playoutSyncMoves = Boolean.parseBoolean(attrs.getNamedItem("playoutSyncMoves").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            player.playoutSyncMoves = false;
        }
        
        if (attrs.getNamedItem("LGRForgetting") != null) {
            try {
                player.LGRForgetting = Boolean.parseBoolean(attrs.getNamedItem("LGRForgetting").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            player.LGRForgetting = true;
        }
        
        if (attrs.getNamedItem("alphaRandom") != null) {
            try {
                player.alphaRandom = Double.parseDouble(attrs.getNamedItem("alphaRandom").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            player.alphaRandom = 0.1;
        }

        player.playout1 = new LGRPlayout(13, player.playoutSyncMoves, player.LGRForgetting);
        player.playout2 = new NGramPlayout(13, player.alphaRandom, player.playoutSyncMoves);
        player.select = SelectionStrategy.RANDOM;//new SelectionStrategy.LGRRandomSelection(player.playout.getLGRTables(), player.LGRSyncMoves);

        if (attrs.getNamedItem("maxTurnTime") != null) {
            try {
                player.MAX_TURN_TIME = Long.parseLong(attrs.getNamedItem("maxTurnTime").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (attrs.getNamedItem("syncMoves") != null) {
            try {
                player.syncMoves = Boolean.parseBoolean(attrs.getNamedItem("syncMoves").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            player.syncMoves = false;
        }

        if (attrs.getNamedItem("reqSimulations") != null) {
            try {
                player.reqSimulations = Integer.parseInt(attrs.getNamedItem("reqSimulations").getNodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        player.description = String.format("select=%s playout1=%s playout2=%s cut=%s final=%s expCut=" + player.applyExpansionCutOff + " simCut=" + player.applySimulationCutOff
                + " alphaRandom = " + player.alphaRandom + "playoutSyncMoves: " + player.playoutSyncMoves + " LGRForgetting:" + player.LGRForgetting + " ", player.select, 
                player.playout1, player.playout2, player.cutOff, player.finalMove);
        return player;
    }

    @Override
    public LGRPBMCTSPlayer clone() {
        LGRPBMCTSPlayer player = new LGRPBMCTSPlayer(getName(), getNumber(), getColor());
        player.select = select;
        player.finalMove = finalMove;
        player.cutOff = cutOff;
        player.playout1 = playout1;
        player.playout2 = playout2;
        player.spaceEval = spaceEval;
        player.description = description;
        player.applyExpansionCutOff = applyExpansionCutOff;
        player.applySimulationCutOff = applySimulationCutOff;
        player.applySpaceEstimation = applySpaceEstimation;
        player.MAX_TURN_TIME = MAX_TURN_TIME;
        player.alphaRandom = alphaRandom;
        player.playoutSyncMoves = playoutSyncMoves;
        player.LGRForgetting = LGRForgetting;
        player.reqSimulations = reqSimulations;
        return player;
    }

    public static Color blend(Color c1, Color c2, double v) {
        double v2 = 1 - v;
        return c1 == null ? (c2 == null ? null : c2)
                : c2 == null ? c1
                : new Color(Math.min(255, (int) (c1.getRed() * v2 + c2.getRed() * v)),
                Math.min(255, (int) (c1.getGreen() * v2 + c2.getGreen() * v)),
                Math.min(255, (int) (c1.getBlue() * v2 + c2.getBlue() * v)));
    }
}
